Skip to content

<fix>[storage]: return group tree in APIQueryVolumeSnapshotTree#4144

Open
zstack-robot-1 wants to merge 3 commits into
zsv_5.1.0from
sync/tao.gan/5.1.0-ZSV-4950@@2
Open

<fix>[storage]: return group tree in APIQueryVolumeSnapshotTree#4144
zstack-robot-1 wants to merge 3 commits into
zsv_5.1.0from
sync/tao.gan/5.1.0-ZSV-4950@@2

Conversation

@zstack-robot-1
Copy link
Copy Markdown
Collaborator

Add groupTrees field to APIQueryVolumeSnapshotTreeReply, built from
VolumeSnapshotGroupVO topology with vote-majority parent resolution.
Extract build logic into VolumeSnapshotGroupTreeBuilder.

Resolves: ZSV-9792

Change-Id: I71616f6f6f6c637a6167637863727575746f616c

sync from gitlab !10048

Add groupTrees field to APIQueryVolumeSnapshotTreeReply, built from
VolumeSnapshotGroupVO topology with vote-majority parent resolution.
Extract build logic into VolumeSnapshotGroupTreeBuilder.

Resolves: ZSV-9792

Change-Id: I71616f6f6f6c637a6167637863727575746f616c
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 29, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a07f0c08-0615-4e6a-b52b-450906422bb7

📥 Commits

Reviewing files that changed from the base of the PR and between cb342d7 and c91dbf1.

📒 Files selected for processing (2)
  • storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java
  • storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java
💤 Files with no reviewable changes (1)
  • storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java

Walkthrough

该 PR 为卷快照查询接口增加“快照组树”支持:新增响应字段、库存模型、组树构建器、管理器集成,并重构删除后组清理逻辑。

Changes

卷快照组树查询端到端实现

Layer / File(s) Summary
API 响应契约与类型定义
header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotTreeReply.java, header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotTreeReplyDoc_zh_cn.groovy
APIQueryVolumeSnapshotTreeReply 中新增 groupTrees 字段并补充中文文档绑定 VolumeSnapshotGroupTreeInventory
快照组树库存数据模型
header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeInventory.java, .../VolumeSnapshotGroupTreeInventoryDoc_zh_cn.groovy, .../VolumeSnapshotGroupTreeRefInventory.java, .../VolumeSnapshotGroupTreeRefInventoryDoc_zh_cn.groovy
新增 VolumeSnapshotGroupTreeInventoryVolumeSnapshotGroupTreeRefInventory POJO 及中文文档,描述节点字段、children 与 refs 结构和访问器。
快照组树构建引擎
storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java
实现 builder:查询组与引用、加载实时快照、构建 parentMap 与 snap→group 映射、投票选父组并组装排序森林,标记 current,并提供 volumeUuid 条件提取与根 VM 查询方法。
API 响应封装与集成
storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java
在 marshalReplyMessageBeforeSending 中拆分为 marshalVolumeTrees 与 marshalGroupTrees:从请求提取 volumeUuid、验证可见性并定位根 VM,调用 builder 填充分组树到 reply。
快照组清理逻辑重构
storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotTreeBase.java
重构 ungroupAfterDeleted:先标记引用为 snapshotDeleted,再逐组统计剩余未删除引用,仅在计数为 0 时删除归档元数据组并批量移除组记录。

Sequence Diagrams

sequenceDiagram
  participant Client as 查询客户端
  participant Manager as VolumeSnapshotManagerImpl
  participant Builder as VolumeSnapshotGroupTreeBuilder
  participant DB as 数据库

  Client->>Manager: APIQueryVolumeSnapshotTreeMsg (含 volumeUuid 条件)
  Manager->>Manager: marshalReplyMessageBeforeSending
  Manager->>Manager: marshalVolumeTrees
  Manager->>Manager: marshalGroupTrees
  Manager->>Manager: 提取 volumeUuid 条件并验证可见性
  Manager->>DB: 验证 volumeUuid 是否在返回 inventories 列表中
  alt 可见且定位到根 VM
    Manager->>Builder: buildForVm(vmInstanceUuid, visibleVolumeUuids)
    Builder->>DB: 查询 VolumeSnapshotGroup 与 VolumeSnapshotGroupRef
    Builder->>DB: 加载未删除快照 VO
    Builder->>Builder: 构建 parentMap 与 snapToGroup 映射
    Builder->>Builder: 解析父组(投票选举)
    Builder->>Builder: 组装森林并标记 current
    Builder-->>Manager: 返回 List<VolumeSnapshotGroupTreeInventory>
    Manager->>Manager: reply.setGroupTrees(...)
  else 不匹配或失败
    Manager->>Manager: 不设置 groupTrees
  end
  Manager-->>Client: APIQueryVolumeSnapshotTreeReply (inventories + groupTrees?)
Loading

预估代码审查工作量

🎯 4 (Complex) | ⏱️ ~45 分钟

诗歌

🐰 我在代码林中挖洞寻根,
快照成群绕树盘旋,
投票选父枝叶整齐,
删除时数清零方归根,
小兔鼓掌,树影更温暖。

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 标题准确反映了拉取请求的主要变更,即向APIQueryVolumeSnapshotTreeReply添加返回分组树的功能。
Description check ✅ Passed 描述清晰地说明了变更内容,包括新增groupTrees字段、基于VolumeSnapshotGroupVO拓扑构建树、父级解析策略以及相关的重构和问题追踪信息。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sync/tao.gan/5.1.0-ZSV-4950@@2

Comment @coderabbitai help to get the list of available commands and usage tips.

@MatheMatrix MatheMatrix force-pushed the sync/tao.gan/5.1.0-ZSV-4950@@2 branch from 1137ca5 to 8f43cbe Compare May 29, 2026 06:27
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java`:
- Around line 267-281: In markCurrent(Map<String,
VolumeSnapshotGroupTreeInventory> groupNodeMap) ensure deterministic selection
and reset: iterate all VolumeSnapshotGroupTreeInventory entries to
setCurrent(false) first, then pick the newest by createDate and, when createDate
is equal or both null, use a stable tie-breaker such as comparing a stable
identifier (e.g., node.getUuid() or node.getName()) via String.compareTo to
deterministically choose one; finally call setCurrent(true) on the chosen newest
node. This touches the markCurrent method and VolumeSnapshotGroupTreeInventory
usage.

In
`@storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java`:
- Around line 1368-1390: marshalGroupTrees currently writes group trees from
groupTreeBuilder.buildForVm(vmInstanceUuid) into the reply without applying
session-level visibility pruning, which can leak snapshot details; update this
flow to pass the caller session (from APIQueryVolumeSnapshotTreeMsg or its
session/context) into the VolumeSnapshotGroupTreeBuilder and use a session-aware
builder method (e.g., buildForVm(session, vmInstanceUuid) or trimBySession(...)
inside VolumeSnapshotGroupTreeBuilder) so that snapshots the session cannot see
are reduced to only uuid (preserving the “no access -> only uuid” contract)
before calling reply.setGroupTrees; ensure the builder enforces field-level
trimming for unauthorized sessions.

In
`@storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotTreeBase.java`:
- Around line 2148-2182: The deletion path in VolumeSnapshotTreeBase marks
VolumeSnapshotGroupRefVO rows for snapshots in the local `snapshots` list as
snapshotDeleted=true but misses group refs for memory snapshots removed later in
cleanUpMemorySnapshots(), leaving remaining>0 and preventing group disband; fix
by also marking group refs for those memory snapshots as deleted—either include
memorySnapshotsOfDescendants' UUIDs when building `uuids` (or perform an
additional SQL.New(VolumeSnapshotGroupRefVO.class)...in(...)
.set(snapshotDeleted,true).update() for the memory snapshot UUIDs) before
computing `remaining` and deleting groups, so VolumeSnapshotGroupRefVO rows for
memory snapshots are set snapshotDeleted=true alongside regular snapshots.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 94ca3d14-5ce9-4116-b74b-d291930fa4d9

📥 Commits

Reviewing files that changed from the base of the PR and between 8740fbd and 8f43cbe.

📒 Files selected for processing (9)
  • header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotTreeReply.java
  • header/src/main/java/org/zstack/header/storage/snapshot/APIQueryVolumeSnapshotTreeReplyDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeInventory.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeInventoryDoc_zh_cn.groovy
  • header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeRefInventory.java
  • header/src/main/java/org/zstack/header/storage/snapshot/group/VolumeSnapshotGroupTreeRefInventoryDoc_zh_cn.groovy
  • storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotManagerImpl.java
  • storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotTreeBase.java
  • storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java

Comment on lines +267 to +281
private void markCurrent(Map<String, VolumeSnapshotGroupTreeInventory> groupNodeMap) {
VolumeSnapshotGroupTreeInventory newest = null;
for (VolumeSnapshotGroupTreeInventory node : groupNodeMap.values()) {
if (newest == null) {
newest = node;
continue;
}
if (node.getCreateDate() != null && (newest.getCreateDate() == null
|| node.getCreateDate().after(newest.getCreateDate()))) {
newest = node;
}
}
if (newest != null) {
newest.setCurrent(true);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

current 选择增加稳定的并列判定

Line 274-276 只按 createDate 比较;当时间相同会受 HashMap 遍历顺序影响,current 结果不稳定,可能导致查询结果抖动。

🔧 建议修复
     private void markCurrent(Map<String, VolumeSnapshotGroupTreeInventory> groupNodeMap) {
         VolumeSnapshotGroupTreeInventory newest = null;
         for (VolumeSnapshotGroupTreeInventory node : groupNodeMap.values()) {
             if (newest == null) {
                 newest = node;
                 continue;
             }
-            if (node.getCreateDate() != null && (newest.getCreateDate() == null
-                    || node.getCreateDate().after(newest.getCreateDate()))) {
+            if (node.getCreateDate() != null && (newest.getCreateDate() == null
+                    || node.getCreateDate().after(newest.getCreateDate())
+                    || (node.getCreateDate().equals(newest.getCreateDate())
+                        && node.getUuid().compareTo(newest.getUuid()) < 0))) {
                 newest = node;
             }
         }
         if (newest != null) {
             newest.setCurrent(true);
         }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@storage/src/main/java/org/zstack/storage/snapshot/group/VolumeSnapshotGroupTreeBuilder.java`
around lines 267 - 281, In markCurrent(Map<String,
VolumeSnapshotGroupTreeInventory> groupNodeMap) ensure deterministic selection
and reset: iterate all VolumeSnapshotGroupTreeInventory entries to
setCurrent(false) first, then pick the newest by createDate and, when createDate
is equal or both null, use a stable tie-breaker such as comparing a stable
identifier (e.g., node.getUuid() or node.getName()) via String.compareTo to
deterministically choose one; finally call setCurrent(true) on the chosen newest
node. This touches the markCurrent method and VolumeSnapshotGroupTreeInventory
usage.

Comment on lines +2148 to +2182
List<String> uuids = snapshots.stream()
.map(VolumeSnapshotInventory::getUuid)
.collect(Collectors.toList());
SQL.New(VolumeSnapshotGroupRefVO.class)
.in(VolumeSnapshotGroupRefVO_.volumeSnapshotUuid, uuids)
.set(VolumeSnapshotGroupRefVO_.snapshotDeleted, true)
.update();

Set<String> touchedGroupUuids = snapshots.stream()
.map(VolumeSnapshotInventory::getGroupUuid)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (touchedGroupUuids.isEmpty()) {
return;
}

List<String> groupsToDelete = new ArrayList<>();
for (String groupUuid : touchedGroupUuids) {
long remaining = Q.New(VolumeSnapshotGroupRefVO.class)
.eq(VolumeSnapshotGroupRefVO_.volumeSnapshotGroupUuid, groupUuid)
.eq(VolumeSnapshotGroupRefVO_.snapshotDeleted, false)
.count();
if (remaining == 0) {
logger.debug(String.format("snapshot group[uuid:%s] all refs deleted, disbanding", groupUuid));
groupsToDelete.add(groupUuid);
}
}

groupUuids.forEach(groupUuid -> vidm.deleteArchiveVmInstanceResourceMetadataGroup(groupUuid));
cleanVmHostBackupFilesForGroup(groupUuids);
dbf.removeByPrimaryKeys(groupUuids, VolumeSnapshotGroupVO.class);
if (groupsToDelete.isEmpty()) {
return;
}

groupsToDelete.forEach(vidm::deleteArchiveVmInstanceResourceMetadataGroup);
cleanVmHostBackupFilesForGroup(groupsToDelete);
dbf.removeByPrimaryKeys(groupsToDelete, VolumeSnapshotGroupVO.class);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

链路删除漏掉了内存快照的分组引用清理。

这里仅把 snapshots 里的 VolumeSnapshotGroupRefVO 标记为 snapshotDeleted=true,但同一条删除链路后面还会在 cleanUpMemorySnapshots() 里硬删 memorySnapshotsOfDescendants。这些 memory snapshot 的 group ref 没有一起置为 deleted,remaining 会一直大于 0,导致包含内存快照的快照组不会解散,VolumeSnapshotGroupVO 和归档元数据都会残留。

💡 修复方向
- if (!cleanup()) {
+ List<VolumeSnapshotInventory> snapshotsToUngroup = new ArrayList<>(currentLeaf.getDescendants());
+ snapshotsToUngroup.addAll(memorySnapshotsOfDescendants);
+ if (!cleanup(snapshotsToUngroup)) {
    ...
- private boolean cleanup() {
+ private boolean cleanup(List<VolumeSnapshotInventory> snapshotsToUngroup) {
     List<VolumeSnapshotInventory> snapshots = currentLeaf.getDescendants();
     ...
     if (cleanup) {
-        ungroupAfterDeleted(snapshots);
+        ungroupAfterDeleted(snapshotsToUngroup);
     }
     return cleanup;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@storage/src/main/java/org/zstack/storage/snapshot/VolumeSnapshotTreeBase.java`
around lines 2148 - 2182, The deletion path in VolumeSnapshotTreeBase marks
VolumeSnapshotGroupRefVO rows for snapshots in the local `snapshots` list as
snapshotDeleted=true but misses group refs for memory snapshots removed later in
cleanUpMemorySnapshots(), leaving remaining>0 and preventing group disband; fix
by also marking group refs for those memory snapshots as deleted—either include
memorySnapshotsOfDescendants' UUIDs when building `uuids` (or perform an
additional SQL.New(VolumeSnapshotGroupRefVO.class)...in(...)
.set(snapshotDeleted,true).update() for the memory snapshot UUIDs) before
computing `remaining` and deleting groups, so VolumeSnapshotGroupRefVO rows for
memory snapshots are set snapshotDeleted=true alongside regular snapshots.

taogan21 added 2 commits May 29, 2026 15:23
Two P1 fixes from review:
1. parent map now includes deleted snapshots so chain traversal can
   cross the deleted point and reach ancestor group.
2. when query is scoped to a single volume, filter refs and groupVOs
   by the visible volume set in the reply to avoid leaking snapshots
   of sibling volumes the caller cannot see.

Resolves: ZSV-9792

Change-Id: I582aa250977fcaf9f5a2f0368d5891518949e32b
Initial fix sliced refs by visibleVolumeUuids, which broke the
incomplete computation (a single-deleted group looked fully-deleted
when only the matching volume's ref survived the filter). Drop the
ref-level slice: cross-account leak is already prevented by the
inventories.anyMatch(volumeUuid) check above, and refs of a single VM
share the VM owner anyway, so returning full per-VM group structure
is the intended semantic.

Resolves: ZSV-9792

Change-Id: I5d58d358c1ac8146ef505e120d491f4e347fb108
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants